AWS PrivateLinkのVPCエンドポイントIDをNGINX Plusで取得する
ども、大瀧です。
AWS PrivateLinkはユーザー独自のエンドポイントサービスを定義し、他のAWSアカウントのVPCやそこから接続するオンプレミス環境にプライベートなネットワークサービスを提供できます。具体的な設定手順は、以下の記事を参考にしてください。
エンドポイントサービスはNLB(Network Load Balancer)を経由して利用するため、トラフィックの接続元IPアドレスはNLBのPrivate IPになってしまいます。接続してくるクライアントの情報を取得する手段としてProxy Protocolバージョン2を有効にし、PrivateLinkが付与するそのヘッダから情報を得ることが出来ます。
Proxy ProtocolはTCPの拡張なので、NLBとTCPセッションを確立するサーバープロセスでそのヘッダを読みこまなければなりません。サーバーアプリケーションであればヘッダの読み込み&パースの実装は容易ですが、管理や運用の都合でリバースプロキシなどのミドルウェアで対応したいケースが多いと思います。Proxy Protocolバージョン2をサポートするリバースプロキシはHAProxyやnginx、Envoyなど複数ありますが、PrivateLinkが付与するVPCエンドポイントIDはTLV(Type-Length-Value)形式のカスタムフィールドであり、これまでそれをサポートするミドルウェアを見つけられていませんでした。
先月、nginxの有償版NGINX PlusがProxy Protocolバージョン2のカスタムフィールドに対応しVPCエンドポイントIDを参照できるようになったので、試してみた様子をレポートします。
NGINX Plusのセットアップ
- NGINX Plusライセンス : フリートライアル
- NGINX Plusバージョン : R16(Nginx 1.15.2)
- Linux OS : Amazon Linux 2
NGINX Plusには30日間のフリートライアルがあるので、今回はそれを利用します。NGINX Plusのサイトでフリートライアルを申請するとアクティベーションのEメールが届くので、メール記載のリンクからカスタマーポータルサイトにアクセス、クライアント証明書の2つのファイルをダウンロードします。
そのあとの手順は以下のドキュメントに従います。
今回はAmazon Linux 2なので、それに対応するyumのリポジトリファイルを配置、先ほどダウンロードしたクライアント証明書をリポジトリから参照してyum経由でライセンス認証が行われる仕組みのようです。
$ sudo mkdir /etc/ssl/nginx $ sudo yum install -y ca-certificates (略) $ sudo wget -P /etc/yum.repos.d https://cs.nginx.com/static/files/nginx-plus-amazon2.repo (略) $ sudo mv nginx-repo.crt nginx-repo.key /etc/ssl/nginx/ $ sudo yum install nginx-plus 読み込んだプラグイン:extras_suggestions, langpacks, priorities, update-motd nginx-plus | 2.9 kB 00:00:00 nginx-plus/2/x86_64/primary_db | 27 kB 00:00:01 依存性の解決をしています --> トランザクションの確認を実行しています。 ---> パッケージ nginx-plus.x86_64 0:16-1.amzn2.ngx を インストール --> 依存性解決を終了しました。 依存性を解決しました ============================================================================================================================================================================================================================================== Package アーキテクチャー バージョン リポジトリー 容量 ============================================================================================================================================================================================================================================== インストール中: nginx-plus x86_64 16-1.amzn2.ngx nginx-plus 3.4 M トランザクションの要約 ============================================================================================================================================================================================================================================== インストール 1 パッケージ 総ダウンロード容量: 3.4 M インストール容量: 8.5 M Is this ok [y/d/N]: y Downloading packages: nginx-plus-16-1.amzn2.ngx.x86_64.rpm | 3.4 MB 00:00:02 Running transaction check Running transaction test Transaction test succeeded Running transaction インストール中 : nginx-plus-16-1.amzn2.ngx.x86_64 1/1 ---------------------------------------------------------------------- Thank you for using NGINX! Please find the documentation for NGINX Plus here: /usr/share/nginx/html/nginx-modules-reference.pdf NGINX Plus is proprietary software. EULA and License information: /usr/share/doc/nginx-plus/ For support information, please see: https://www.nginx.com/support/ ---------------------------------------------------------------------- 検証中 : nginx-plus-16-1.amzn2.ngx.x86_64 1/1 インストール: nginx-plus.x86_64 0:16-1.amzn2.ngx 完了しました! $
一度インストールしてしまえば、オープンソース版Nginxと同様に扱うことができます。
$ sudo service nginx start Redirecting to /bin/systemctl start nginx.service $ sudo service nginx status Redirecting to /bin/systemctl status nginx.service ● nginx.service - NGINX Plus - high performance web server Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled) Drop-In: /usr/lib/systemd/system/nginx.service.d └─php-fpm.conf Active: active (running) since 月 2018-10-15 04:13:59 UTC; 5h 17min ago Docs: https://www.nginx.com/resources/ Process: 5335 ExecStop=/bin/kill -s TERM $MAINPID (code=exited, status=0/SUCCESS) Process: 5511 ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf (code=exited, status=0/SUCCESS) Process: 5495 ExecStartPre=/usr/libexec/nginx-plus/check-subscription (code=exited, status=0/SUCCESS) Main PID: 5514 (nginx) CGroup: /system.slice/nginx.service ├─5514 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf ├─5515 nginx: worker process └─5516 nginx: worker process 10月 15 04:13:59 ip-172-31-15-203.ap-northeast-1.compute.internal systemd[1]: Starting NGINX Plus - high performance web server... 10月 15 04:13:59 ip-172-31-15-203.ap-northeast-1.compute.internal check-subscription[5495]: Your trial subscription will expire in 30 days 10月 15 04:13:59 ip-172-31-15-203.ap-northeast-1.compute.internal systemd[1]: Started NGINX Plus - high performance web server. $
これでNGINX Plusが起動しました。
VPCエンドポイントIDの参照
まずは、リクエストをProxy Protocolバージョン2で受け取るためにlisten
ディレクティブにproxy_protocol
を指定します。
server { listen 80 proxy_protocol; #listen 80 default_server; server_name localhost; :(略)
あとはヘッダをNGINX Plusが読み込んで変数に格納するので、VPCエンドポイントIDに対応する変数名$proxy_protocol_tlv_0xEA
で参照すればOKです。今回アクセスログに以下の様に追記してみます。
http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$proxy_protocol_addr" "$proxy_protocol_tlv_0xEA"'; #'"$http_user_agent" "$http_x_forwarded_for"';
$proxy_protocol_addr
はProxy Protocolヘッダに含まれるクライアントIPアドレスを指します。NLBはX-Forwarded-For
ヘッダを操作しないので、その代わりに入れてみました。アクセスログを見てみると。。。
$ tail /var/log/nginx/access.log 172.31.4.8 - - [15/Oct/2018:03:57:33 +0000] "GET /h/ HTTP/1.1" 400 48 "-" "curl/7.55.1" "172.31.1.203" "\x01vpce-0123456789abcdef0" 172.31.4.8 - - [15/Oct/2018:03:57:34 +0000] "GET /h/ HTTP/1.1" 400 48 "-" "curl/7.55.1" "172.31.1.203" "\x01vpce-0123456789abcdef0" 172.31.4.8 - - [15/Oct/2018:03:58:58 +0000] "GET /h/ HTTP/1.1" 400 48 "-" "curl/7.55.1" "172.31.1.203" "\x01vpce-0123456789abcdef0" :
172.31.1.203
がアクセス元のIPアドレス、vpce-0123456789abcdef0
(架空のID)がVPCエンドポイントIDです。きちんとログに残せていますね。
アプリケーションへのVPCエンドポイントIDの伝達
エンドポイントサービスでマルチテナントなサービスを提供するのであれば、顧客ごとの認証やアクセス制限をアプリケーションで実装したいところです。そこでNGINX Plusで取得したVPCエンドポイントIDをリクエストヘッダにセットし、プロキシのアップストリーム(アプリケーション)に渡す構成を考えてみます。まずはproxy_set_header
ディレクティブで任意のヘッダに$proxy_protocol_tlv_0xEA
をセットしてみたのですが、NGINX Plusが400 Bad Request
を返すようになってしまいました。
$ curl vpce-0123456789abcdef0-XXXXXXXX.vpce-svc-0123456789abcdef0.ap-northeast-1.vpce.amazonaws.com/h/ 400 Bad Request: invalid header value$
NGINX Plusのエラーログでエラー内容を拾おうとしたのですが、debug
レベルにしてもそれらしいログが出て来ずハマりました。いろいろ試してみたところ変数の値に含まれる\x01
が悪さをしている気がしてきたので、以下のように\x01
を取り除いてリクエストヘッダにセットするようにしてみました。
server { listen 80 proxy_protocol; #listen 80 default_server; server_name localhost; if ($proxy_protocol_tlv_0xEA ~* "\x01(.*)") { set $vpce_id $1; } location /h/ { proxy_pass http://127.0.0.1:3000; proxy_set_header X-Forwarded-By $vpce_id; } :(略)
アプリケーションサーバーはGolangで実装し、ヘッダをレスポンスとして返すようにしてみました。
package main import ( "fmt" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Value: " + r.Header.Get("X-Forwarded-By")) } func main() { http.HandleFunc("/", handler) http.ListenAndServe(":3000", nil) }
あらためてリクエストを送ってみると...
$ curl vpce-0123456789abcdef0-XXXXXXXX.vpce-svc-0123456789abcdef0.ap-northeast-1.vpce.amazonaws.com/h/ Value: vpce-0123456789abcdef0$
NGINX Plusが読んだVPCエンドポイントIDを、きちんとアプリケーションサーバーで受けて取れていますね!
まとめ
NGINX PlusでPrivateLinkのVPCエンドポイントIDを参照する様子をご紹介しました。VPCエンドポイントIDがわかればAWSアカウントIDに対応づけることもできるので、マルチテナント、マルチアカウントでエンドポイントサービスをするときにはリバースプロキシとしてNGINX Plusを検討してみるのが良いと思います!